﻿namespace EasyWeb
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Text;
    using System.Threading.Tasks;
    using System.Web.Routing;

    internal static partial class EWHelpers
    {
        public static object ToObject(this IDictionary<string, object> instance)
        {
            if (instance != null && instance.Count > 0)
            {
                return DynamicObjectHelpers.Create(instance);
            }

            return null;
        }

        public static IDictionary<string, object> ToDictionary(this object obj)
        {
            var result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

            if (obj != null)
            {
                foreach (var property in obj.GetType().GetTypeInfo().GetProperties())
                {
                    result.Add(property.Name, property.GetValue(obj));
                }
            }

            return result;
        }

        public static RouteValueDictionary ToRouteDictionary(this object obj)
        {
            var result = new RouteValueDictionary();

            if (obj != null)
            {
                foreach (var property in obj.GetType().GetTypeInfo().GetProperties())
                {
                    result.Add(property.Name, property.GetValue(obj));
                }
            }

            return result;
        }

        private static class DynamicObjectHelpers
        {
            private static readonly ConcurrentDictionary<DictionaryKey, DictionaryValue> cache = new ConcurrentDictionary<DictionaryKey, DictionaryValue>();
            private static readonly AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(Guid.NewGuid().ToString()), AssemblyBuilderAccess.Run);
            private static readonly ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(Guid.NewGuid().ToString());

            public static object Create(IDictionary<string, object> dictionary)
            {
                var dictionaryKey = new DictionaryKey(dictionary);
                var dictionaryValue = cache.GetOrAdd(dictionaryKey, CreateDictionaryValue);

                var obj = EWHelpers.TypeHelpers.Create(dictionaryValue.type);

                foreach (var item in dictionary)
                {
                    var propertyInfo = dictionaryValue.GetProperty(item.Key);
                    propertyInfo.SetValue(obj, item.Value);
                }

                return obj;
            }

            private static DictionaryValue CreateDictionaryValue(DictionaryKey key)
            {
                var result = new DictionaryValue();

                var typeBuilder = moduleBuilder.DefineType(Guid.NewGuid().ToString(), TypeAttributes.Public | TypeAttributes.Class);

                foreach (var pair in key.Pairs)
                {
                    var propertyName = pair.propertyName;
                    var propertyType = pair.propertyType;

                    var fieldBuilder = typeBuilder.DefineField("_<>_" + propertyName, propertyType, FieldAttributes.Private | FieldAttributes.SpecialName | FieldAttributes.RTSpecialName);

                    var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault | PropertyAttributes.SpecialName | PropertyAttributes.RTSpecialName, propertyType, null);

                    var getPropBuilder = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
                    var getIL = getPropBuilder.GetILGenerator();
                    getIL.Emit(OpCodes.Ldarg_0);
                    getIL.Emit(OpCodes.Ldfld, fieldBuilder);
                    getIL.Emit(OpCodes.Ret);

                    var setPropBuilder = typeBuilder.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig, null, new[] { propertyType });
                    var setIL = setPropBuilder.GetILGenerator();
                    setIL.Emit(OpCodes.Ldarg_0);
                    setIL.Emit(OpCodes.Ldarg_1);
                    setIL.Emit(OpCodes.Stfld, fieldBuilder);
                    setIL.Emit(OpCodes.Ret);

                    propertyBuilder.SetGetMethod(getPropBuilder);
                    propertyBuilder.SetSetMethod(setPropBuilder);
                }

                result.type = typeBuilder.CreateType();

                foreach (var propertyInfo in result.type.GetProperties())
                {
                    result.properties.Add(propertyInfo);
                }

                return result;
            }

            private class DictionaryKey
            {
                private readonly List<PropertyNameTypePair> pairs = new List<PropertyNameTypePair>();

                public DictionaryKey(IDictionary<string, object> dictionary)
                {
                    foreach (var item in dictionary)
                    {
                        if (item.Value != null)
                        {
                            this.pairs.Add(new PropertyNameTypePair()
                            {
                                propertyName = item.Key,
                                propertyType = item.Value.GetType()
                            });
                        }
                    }
                }

                public IEnumerable<PropertyNameTypePair> Pairs
                {
                    get { return this.pairs; }
                }

                public override bool Equals(object obj)
                {
                    var another = (DictionaryKey)obj;

                    if (this.pairs.Count != another.pairs.Count)
                    {
                        return false;
                    }

                    foreach (var pair in this.pairs)
                    {
                        if (!another.pairs.Contains(pair))
                        {
                            return false;
                        }
                    }

                    return true;
                }

                public override int GetHashCode()
                {
                    var result = 0;

                    foreach (var pair in this.pairs)
                    {
                        result ^= pair.GetHashCode();
                    }

                    return result;
                }
            }

            private class PropertyNameTypePair
            {
                public string propertyName;
                public Type propertyType;

                public override bool Equals(object obj)
                {
                    var another = (PropertyNameTypePair)obj;
                    return this.propertyName == another.propertyName && this.propertyType == another.propertyType;
                }

                public override int GetHashCode()
                {
                    return this.propertyName.GetHashCode() ^ this.propertyType.GetHashCode();
                }
            }

            private class DictionaryValue
            {
                public readonly ICollection<PropertyInfo> properties = new List<PropertyInfo>();
                public Type type;

                public PropertyInfo GetProperty(string name)
                {
                    return this.properties.First(p => p.Name == name);
                }
            }
        }
    }
}
